using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;

using Evaluant.Uss.Collections;
using Evaluant.Uss.ObjectContext;
using Evaluant.Uss.ObjectContext.Descriptors;
using Evaluant.Uss.Models;
using System.Runtime.CompilerServices;

namespace Evaluant.Uss.ObjectContext.DynamicProxy
{

	public class ReflectionEntityGetter
	{
		private IPersistentDescriptor _Descriptor;
		private Model _Model;

		/// <summary>
		/// Get only the specified properties
		/// </summary>
		public ReflectionEntityGetter(IPersistentDescriptor descriptor, Model model)
		{
			_Descriptor = descriptor;
			_Model = model;
		}

		public Entity Get(object target, Dictionary<object, Entity> locallyProcessed, Dictionary<int, Entity> globallyProcessed)
		{
			if (locallyProcessed.ContainsKey(target))
				return locallyProcessed[target];

			Type targetType = target.GetType();

			Entity e = null;

			if (target is IPersistableProxy)
			{
				IPersistableProxy proxy = target as IPersistableProxy;

				// Makes an update on the current object so that the entity reflects its private values
				// Also calls Update() recursively on all IPersistable only related objects
				proxy.Update();

				e = proxy.Entity;

				// A cyclic reference can already process the current item
				if (!locallyProcessed.ContainsKey(target))
					locallyProcessed.Add(target, e);

				// Explicitly updates for none IPersistable references
				foreach (PropertyDescriptor prop in _Descriptor.GetPersistentProperties(targetType))
				{
					if (!prop.IsEntity)
					{
						continue;
					}

					// Ignores this member (attribute or reference) if not in metadata
					if (_Model.GetAttribute(e.Type, prop.PropertyName) == null
						&& _Model.GetReference(e.Type, prop.PropertyName) == null)
					{
						continue;
					}

					object fieldValue = prop.GetValue(targetType, target);

					if (fieldValue == null)
					{
						continue;
					}

					if (prop.IsList || prop.IsGenericList)
					{
						foreach (object o in (IEnumerable)fieldValue)
						{
							if (o is IPersistable)
							{
								continue;
							}

							Get(o, locallyProcessed, globallyProcessed);
						}
					}
					else
					{
						if (fieldValue is IPersistable)
						{
							continue;
						}

						Get(fieldValue, locallyProcessed, globallyProcessed);
					}
				}

				return e;
			}
			else if (target is IPersistable)
			{
				e = ((IPersistable)target).Entity;

				globallyProcessed.Add(RuntimeHelpers.GetHashCode(target), e);
				locallyProcessed.Add(target, e);

				return e;
			}

			// the new object has already been processed ?
			if (globallyProcessed.ContainsKey(RuntimeHelpers.GetHashCode(target)))
			{
				e = globallyProcessed[RuntimeHelpers.GetHashCode(target)];
				if (e.State == State.UpToDate)
					return e;
			}
			else
			{
				e = new Entity(ObjectContext.ConvertNamespaceDomainToEuss(targetType));
				globallyProcessed.Add(RuntimeHelpers.GetHashCode(target), e);
			}

			locallyProcessed.Add(target, e);

			foreach (PropertyDescriptor prop in _Descriptor.GetPersistentProperties(targetType))
			{
				// Ignores this member (attribute or reference) if not in metadata
				if (_Model.GetAttribute(e.Type, prop.PropertyName) == null
					&& _Model.GetReference(e.Type, prop.PropertyName) == null)
					continue;

				object fieldValue = prop.GetValue(targetType, target);

				if (fieldValue == null)
					continue;

				if (!prop.IsEntity)
				{
					if (prop.Type.IsEnum)
						if (_Model.GetAttribute(targetType.FullName.Replace('.', ':'), prop.PropertyName).Type == typeof(string))
							e.SetValue(prop.PropertyName, Enum.GetName(prop.Type, fieldValue));
						else
							e.SetValue(prop.PropertyName, (int)fieldValue);
					else
						e.SetValue(prop.PropertyName, fieldValue);
				}
				else
				{
					if (e.State == State.New)
					{
						if (prop.IsList || prop.IsGenericList)
						{
							foreach (object o in (IEnumerable)fieldValue)
							{
								Entity r = Get(o, locallyProcessed, globallyProcessed);
								e.AddValue(prop.PropertyName, r);
							}
						}
						else
						{
							e.SetValue(prop.PropertyName, Get(fieldValue, locallyProcessed, globallyProcessed));
						}
					}
					else
					{
						if (prop.IsList || prop.IsGenericList)
						{
							EntitySet newRels = new EntitySet();

							foreach (object o in (IEnumerable)fieldValue)
							{
								Entity r = Get(o, locallyProcessed, globallyProcessed);
								newRels.Add(r);
							}

							// Removes removed entities
							foreach (Entity r in e.GetEntitySet(prop.PropertyName))
							{
								if (!newRels.Contains(r))
									e.RemoveReference(prop.PropertyName, r.Id);
							}

							EntitySet oldRels = e.GetEntitySet(prop.PropertyName);

							// Add new references
							foreach (Entity r in newRels)
							{
								if (!oldRels.Contains(r))
									e.AddValue(prop.PropertyName, r);
							}
						}
						else
						{
							Entity r = Get(fieldValue, locallyProcessed, globallyProcessed);

							if (r.State == State.New)
								e.AddValue(prop.PropertyName, r);
							else
							{
								Entity old = e.GetEntity(prop.PropertyName);

								if (old == null || r.Id != old.Id)
								{
									if (old != null)
										e.RemoveReference(prop.PropertyName, old.Id);
									e.AddValue(prop.PropertyName, r);
								}
							}

						}

					}
				}
			}

			// Updates the Entity's id to be serialized with the one in the object (for assigned values)
			PropertyDescriptor idPropertyDesc = _Descriptor.GetIdDescriptor(targetType);
			if (idPropertyDesc != null && e.State == State.New)
			{
				object idFieldValue = idPropertyDesc.GetValue(targetType, target);

				if (idPropertyDesc.Type == typeof(string))
				{
					if (idFieldValue != null)
					{
						e.Id = idFieldValue.ToString();
					}
					else
					{
						idPropertyDesc.SetValue(targetType, target, e.Id);
					}
				}
				else if (idPropertyDesc.Type == typeof(Int32) && (int)idFieldValue != 0)
				{
					e.Id = idFieldValue.ToString();
				}
			}

			return e;
		}
	}
}
